/*
 * #%L
 * HAPI FHIR JAX-RS Server
 * %%
 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package ca.uhn.fhir.jaxrs.server;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsExceptionInterceptor;
import ca.uhn.fhir.jaxrs.server.util.JaxRsMethodBindings;
import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest;
import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import jakarta.interceptor.Interceptors;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.hl7.fhir.instance.model.api.IBaseResource;

import java.io.IOException;
import java.net.URL;

/**
 * This server is the abstract superclass for all resource providers. It exposes
 * a large amount of the fhir api functionality using JAXRS
 * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
 */
@Produces({
	MediaType.APPLICATION_JSON,
	MediaType.APPLICATION_XML,
	MediaType.TEXT_PLAIN,
	Constants.CT_FHIR_JSON,
	Constants.CT_FHIR_XML
})
@Consumes({
	MediaType.APPLICATION_FORM_URLENCODED,
	MediaType.APPLICATION_JSON,
	Constants.CT_FHIR_JSON,
	Constants.CT_FHIR_XML,
	Constants.CT_FHIR_JSON_NEW,
	Constants.CT_FHIR_XML_NEW,
	"application/octet-stream"
})
@Interceptors(JaxRsExceptionInterceptor.class)
public abstract class AbstractJaxRsResourceProvider<R extends IBaseResource> extends AbstractJaxRsProvider
		implements IRestfulServer<JaxRsRequest>, IResourceProvider {

	/** the method bindings for this class */
	private final JaxRsMethodBindings theBindings;

	/**
	 * The default constructor. The method bindings are retrieved from the class
	 * being constructed.
	 */
	protected AbstractJaxRsResourceProvider() {
		super();
		theBindings = JaxRsMethodBindings.getMethodBindings(this, getClass());
	}

	/**
	 * Provides the ability to specify the {@link FhirContext}.
	 * @param ctx the {@link FhirContext} instance.
	 */
	protected AbstractJaxRsResourceProvider(final FhirContext ctx) {
		super(ctx);
		theBindings = JaxRsMethodBindings.getMethodBindings(this, getClass());
	}

	/**
	 * This constructor takes in an explicit interface class. This subclass
	 * should be identical to the class being constructed but is given
	 * explicitly in order to avoid issues with proxy classes in a jee
	 * environment.
	 *
	 * @param theProviderClass the interface of the class
	 */
	protected AbstractJaxRsResourceProvider(final Class<? extends AbstractJaxRsProvider> theProviderClass) {
		super();
		theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass);
	}

	/**
	 * This constructor takes in an explicit interface class. This subclass
	 * should be identical to the class being constructed but is given
	 * explicitly in order to avoid issues with proxy classes in a jee
	 * environment.
	 *
	 * @param ctx the {@link FhirContext} instance.
	 * @param theProviderClass the interface of the class
	 */
	protected AbstractJaxRsResourceProvider(
			final FhirContext ctx, final Class<? extends AbstractJaxRsProvider> theProviderClass) {
		super(ctx);
		theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass);
	}

	/**
	 * The base for request for a resource provider has the following form:</br>
	 * {@link AbstractJaxRsResourceProvider#getBaseForServer()
	 * getBaseForServer()} + "/" +
	 * {@link AbstractJaxRsResourceProvider#getResourceType() getResourceType()}
	 * .{@link java.lang.Class#getSimpleName() getSimpleName()}
	 */
	@Override
	public String getBaseForRequest() {
		try {
			return new URL(getUriInfo().getBaseUri().toURL(), getResourceType().getSimpleName()).toExternalForm();
		} catch (final Exception e) {
			// cannot happen
			return null;
		}
	}

	/**
	 * Create a new resource with a server assigned id
	 *
	 * @param resource the body of the post method containing resource being created in a xml/json form
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#create">https://www.hl7. org/fhir/http.html#create</a>
	 */
	@POST
	public Response create(final String resource) throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.POST, RestOperationTypeEnum.CREATE)
				.resource(resource));
	}

	/**
	 * Search the resource type based on some filter criteria
	 *
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
	 */
	@POST
	@Path("/_search")
	public Response searchWithPost() throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.POST, RestOperationTypeEnum.SEARCH_TYPE));
	}

	/**
	 * Search the resource type based on some filter criteria
	 *
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
	 */
	@GET
	public Response search() throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE));
	}

	/**
	 * Update an existing resource based on the given condition
	 * @param resource the body contents for the put method
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#update">https://www.hl7.org/fhir/http.html#update</a>
	 */
	@PUT
	public Response conditionalUpdate(final String resource) throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.PUT, RestOperationTypeEnum.UPDATE)
				.resource(resource));
	}

	/**
	 * Update an existing resource by its id (or create it if it is new)
	 *
	 * @param id the id of the resource
	 * @param resource the body contents for the put method
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#update">https://www.hl7.org/fhir/http.html#update</a>
	 */
	@PUT
	@Path("/{id}")
	public Response update(@PathParam("id") final String id, final String resource) throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.PUT, RestOperationTypeEnum.UPDATE)
				.id(id)
				.resource(resource));
	}

	/**
	 * Delete a resource based on the given condition
	 *
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#delete">https://www.hl7.org/fhir/http.html#delete</a>
	 */
	@DELETE
	public Response delete() throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.DELETE, RestOperationTypeEnum.DELETE));
	}

	/**
	 * Delete a resource
	 *
	 * @param id the id of the resource to delete
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#delete">https://www.hl7.org/fhir/http.html#delete</a>
	 */
	@DELETE
	@Path("/{id}")
	public Response delete(@PathParam("id") final String id) throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.DELETE, RestOperationTypeEnum.DELETE)
				.id(id));
	}

	/**
	 * Read the current state of the resource
	 *
	 * @param id the id of the resource to read
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#read">https://www.hl7.org/fhir/http.html#read</a>
	 */
	@GET
	@Path("/{id : ((?!_history).)*}")
	public Response find(@PathParam("id") final String id) throws IOException {
		return execute(getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.READ)
				.id(id));
	}

	/**
	 * Execute a custom operation
	 *
	 * @param resource the resource to create
	 * @param requestType the type of request
	 * @param id the id of the resource on which to perform the operation
	 * @param operationName the name of the operation to execute
	 * @param operationType the rest operation type
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/operations.html">https://www.hl7.org/fhir/operations.html</a>
	 */
	protected Response customOperation(
			final String resource,
			final RequestTypeEnum requestType,
			final String id,
			final String operationName,
			final RestOperationTypeEnum operationType)
			throws IOException {
		final Builder request = getResourceRequest(requestType, operationType)
				.resource(resource)
				.id(id);
		return execute(request, operationName);
	}

	/**
	 * Retrieve a version of a resource
	 *
	 * @param id the id of the resource
	 * @param version the version of the resource
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
	 */
	@GET
	@Path("/{id}/_history/{version}")
	public Response findVersion(@PathParam("id") final String id, @PathParam("version") final String version)
			throws IOException {
		final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.VREAD)
				.id(id)
				.version(version);
		return execute(theRequest);
	}

	/**
	 * Retrieve the update history for a particular resource
	 *
	 * @param id the id of the resource
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
	 */
	@GET
	@Path("/{id}/_history")
	public Response historyForInstance(@PathParam("id") final String id) throws IOException {
		final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.HISTORY_INSTANCE)
				.id(id);
		return execute(theRequest);
	}

	/**
	 * Retrieve the update history for a particular type
	 *
	 * @return the response
	 * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
	 */
	@GET
	@Path("/_history")
	public Response historyForType() throws IOException {
		final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.HISTORY_TYPE);
		return execute(theRequest);
	}

	/**
	 * Compartment Based Access
	 *
	 * @param id the resource to which the compartment belongs
	 * @param compartment the compartment
	 * @return the repsonse
	 * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
	 * @see <a href="https://www.hl7.org/fhir/compartments.html#compartment">https://www.hl7.org/fhir/compartments.html#compartment</a>
	 */
	@GET
	@Path("/{id}/{compartment : ((?!_history).)*}")
	public Response findCompartment(
			@PathParam("id") final String id, @PathParam("compartment") final String compartment) throws IOException {
		final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE)
				.id(id)
				.compartment(compartment);
		return execute(theRequest, compartment);
	}

	@POST
	@Path("/$validate")
	public Response validate(final String resource) throws IOException {
		return customOperation(
				resource, RequestTypeEnum.POST, null, "$validate", RestOperationTypeEnum.EXTENDED_OPERATION_TYPE);
	}

	/**
	 * Execute the method described by the requestBuilder and methodKey
	 *
	 * @param theRequestBuilder the requestBuilder that contains the information about the request
	 * @param methodKey the key determining the method to be executed
	 * @return the response
	 */
	private Response execute(final Builder theRequestBuilder, final String methodKey) throws IOException {
		final JaxRsRequest theRequest = theRequestBuilder.build();
		final BaseMethodBinding method = getBinding(theRequest.getRestOperationType(), methodKey);
		try {
			return (Response) method.invokeServer(this, theRequest);
		} catch (final Throwable theException) {
			return handleException(theRequest, theException);
		}
	}

	/**
	 * Execute the method described by the requestBuilder
	 *
	 * @param theRequestBuilder the requestBuilder that contains the information about the request
	 * @return the response
	 */
	private Response execute(final Builder theRequestBuilder) throws IOException {
		return execute(theRequestBuilder, JaxRsMethodBindings.DEFAULT_METHOD_KEY);
	}

	/**
	 * Return the method binding for the given rest operation
	 *
	 * @param restOperation the rest operation to retrieve
	 * @param theBindingKey the key determining the method to be executed (needed for e.g. custom operation)
	 * @return
	 */
	protected BaseMethodBinding getBinding(final RestOperationTypeEnum restOperation, final String theBindingKey) {
		return getBindings().getBinding(restOperation, theBindingKey);
	}

	/**
	 * Default: no paging provider
	 */
	@Override
	public IPagingProvider getPagingProvider() {
		return null;
	}

	/**
	 * Default: BundleInclusionRule.BASED_ON_INCLUDES
	 */
	@Override
	public BundleInclusionRule getBundleInclusionRule() {
		return BundleInclusionRule.BASED_ON_INCLUDES;
	}

	@Override
	public PreferReturnEnum getDefaultPreferReturn() {
		return PreferReturnEnum.REPRESENTATION;
	}

	/**
	 * The resource type should return conform to the generic resource included
	 * in the topic
	 */
	@Override
	public abstract Class<R> getResourceType();

	/**
	 * Return the bindings defined in this resource provider
	 *
	 * @return the jax-rs method bindings
	 */
	public JaxRsMethodBindings getBindings() {
		return theBindings;
	}

	/**
	 * Return the request builder based on the resource name for the server
	 * @param requestType the type of the request
	 * @param restOperation the rest operation type
	 * @return the requestbuilder
	 */
	private Builder getResourceRequest(final RequestTypeEnum requestType, final RestOperationTypeEnum restOperation) {
		return getRequest(requestType, restOperation, getResourceType().getSimpleName());
	}
}
